# Java 泛型机制
Java 在 JDK 1.5 的时候引入了泛型( generic ),泛型提高了代码的复用性,同时编译器还加入了编译时类型安全检查机制,可以在编译时发现与泛型声明类型不符合的问题。
Java 泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。
# 泛型的使用
在 Java 泛型中,使用 <T>
来声明泛型,可以使用任意合法的标识符来表示泛型参数类型,也可以使用 <S, U>
这种方式来表示多元泛型。
以下是常见的泛型参数类型标识符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
# 泛型类、接口
泛型类、接口的声明,需要在类名之后添加泛型标识符 <T>
,然后才可能使用 T
作为数据类型。
/**
* 接口结果类,其中的 data 字段是一个泛型,
* 可以满足返回各种不同的结果类型
*
* @author linjinjia
* @date 2023/7/5 10:30
*/
public class Result<T> {
private Integer code;
private T data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
Result<String> result = new Result<>();
result.setCode(200);
result.setData("Success");
// 可以将 data 字段作为字符串去调用其方法
System.out.println(result.getData().length());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 泛型方法
泛型方法的声明,需要在修饰符之后,返回类型之前添加泛型标识符 <T>
,然后才可以使用 T
作为方法的返回值或者参数类型。
/**
* @author linjinjia
* @date 2023/7/5 10:45
*/
public class GenericMethod {
/**
* 通过静态泛型方法获取 Class 的实例
*
* @param clazz 类型
* @param <T> 泛型类型
* @return 泛型对应的实例
* @throws Exception 反射异常
*/
public static <T> T getObject(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
public static void main(String[] args) throws Exception {
GenericMethod gm = GenericMethod.getObject(GenericMethod.class);
System.out.println(gm);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 类型的上下限
在使用泛型的时候,我们可以对参数的类型进行上下界限制,比如参数类型只允许传入某种类型的父类或者子类。
参数类型的上限:
< ? extends T>
这个写法表示参数类型
?
只能是T
或者T
的子类。比如<T extends Number>
表示T
只能是 Number 或者 Number 的子类,Integer、Double 等等。参数类型的下限:
<? super T>
这个写法表示参数类型
?
只能是T
或者T
的父类。比如<T super Integer>
表示T
只能是 Integer 或者 Integer 的父类,Number、Object 。
# 泛型的深入理解
# 类型擦除
Java 的泛型是“伪泛型”,类型信息会在编译器被清除,将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。比如 List<String>
和 List<Integer>
经过编译之后,类型都变为 List
。
下面通过一个例子,结合代码中的注释来感受类型擦除。
public static void main(String[] args) throws Exception {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
strList.add("abc");
intList.add(123);
System.out.println(strList.getClass() == intList.getClass()); // 输出:true
// strList.add(123); // 编译报错
strList.getClass().getMethod("add", Object.class).invoke(strList, 123);
System.out.println(strList); // 输出:[abc, 123]
intList.getClass().getMethod("add", Object.class).invoke(intList, "abc");
System.out.println(intList); // 输出:[123, abc]
int a = intList.get(0);
int b = intList.get(1); // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 第 行输出为 true,说明两个
List
的类型是一样的。 - 第 企图直接往字符串列表加入整数,显然是不行的,会编译报错。
- 第 、 行都是通过反射的方式调用各自的 add 方法添加元素,往字符串列表添加了整数,往整数列表添加了字符串,这是不会报错的,并且可以打印各自的元素。由此也可以看出运行时是不会对添加的元素进行类型检查的。
- 第 行企图从整数列表 intList 中拿出通过反射添加的字符串赋值给整型变量 b,程序直接抛异常提醒 String 不能转型为 Integer。
通过第 点和第 点的比较可以知道在编译器还是有类型检查的,而在运行时没有类型检查。
这是因为通过编译期的类型擦除后,字节码中保留的是原始类型。
# 原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
对于
<T>
这种声明,其原始类型为 Object。对于类似
<T extends Number>
这种声明,其原始类型为 Number。
下面对两种声明进行代码验证
import java.util.Arrays;
public class GenericType {
/**
* 没有指定界限,原始类型为 Object
*
* @param <E> 类型
*/
private static class ObjectType<E> {
/**
* 类型擦除后,参数类型变为 Object
*/
public void add(E e) {
}
}
/**
* 指定上限,原始类型为 Number
*
* @param <E> 类型
*/
private static class NumberType<E extends Number> {
/**
* 类型擦除后,参数类型变为 Number
*/
public void add(E e) {
}
}
public static void main(String[] args) throws Exception {
System.out.println(Arrays.asList(ObjectType.class.getDeclaredMethods()));
System.out.println(Arrays.asList(NumberType.class.getDeclaredMethods()));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
代码的输出结果是:
[public void GenericType$ObjectType.add(java.lang.Object)]
[public void GenericType$NumberType.add(java.lang.Number)]
2
由此可见,二者字节码信息中保存的参数类型是不一样的。因为编译期类型擦除之后,保留的是各自的原始类型,因为 NumberType 类限定了类型,所以参数类型是 Number。
# 获取泛型类的参数类型
在 Java 中,由于类型擦除(Type Erasure)的特性,泛型的参数类型在运行时是不可直接获取的。但是可以通过反射来获取泛型的参数类型。
如果你有一个泛型类,你可以通过 getGenericSuperclass()
方法获取该类的泛型父类。然后通过 ParameterizedType
接口的方法来获取泛型参数类型。
public class GenericType {
private static class ObjectType<E> {
}
public static void main(String[] args) throws Exception {
// ob 实际指向 ObjectType 的子类实例
ObjectType<String> ob = new ObjectType<String>() {
};
// 拿到 ObjectType 的类型
Type superClass = ob.getClass().getGenericSuperclass();
System.out.println("class: " + superClass);
// //getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type generic = ((ParameterizedType) superClass).getActualTypeArguments()[0];
System.out.println("参数类型: " + generic);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
输出:
class: GenericType$ObjectType<java.lang.String>
参数类型: class java.lang.String
2
# 参考文章
- https://juejin.cn/post/6844904163751510030
- https://pdai.tech/md/java/basic/java-basic-x-generic.html